/*
 * Copyright 2008 Zoundry LLC, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 */
package org.zoundry.gaedt.services.applications;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResourceRuleFactory;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.ISchedulingRule;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchManager;
import org.eclipse.debug.core.ILaunchesListener2;
import org.zoundry.gaedt.core.util.ZGaeUtil;
import org.zoundry.gaedt.sdk.ZGaeSDKSchedulingRule;
import org.zoundry.gaedt.services.applications.starters.IZGaeApplicationStarter;
import org.zoundry.gaedt.services.applications.starters.ZGaeApplicationStarterFactory;
import org.zoundry.gaedt.services.applications.updaters.IZGaeApplicationUpdater;
import org.zoundry.gaedt.services.applications.updaters.ZGaeApplicationUpdaterFactory;


/**
 * A service that is responsible for managing a list of automatically discovered
 * Google App Engine applications found in the workspace.
 */
public class ZGaeApplicationsService implements IZGaeApplicationsService, ILaunchesListener2
{
    /** Singleton instance of the applications service. */
    private static final ZGaeApplicationsService sInstance = new ZGaeApplicationsService();

    /**
     * Singleton accessor.
     */
    public static ZGaeApplicationsService getInstance()
    {
        return sInstance;
    }

    /** The set of applications being managed by this service. */
    private Set<IZGaeApplication> mApplications = new HashSet<IZGaeApplication>();
    /** The cache of application starters. */
    private List<IZGaeApplicationStarter> mStarters = new ArrayList<IZGaeApplicationStarter>();
    /** Tracks workspace changes - looks for new/old applications. */
    private ZGaeApplicationResourceTracker mResourceTracker;
    /** The list of observers of this service. */
    private List<IZGaeApplicationsServiceObserver> mObservers = new ArrayList<IZGaeApplicationsServiceObserver>();

    /**
     * Start life-cycle method - called when the plug-in activator starts.
     *
     * @see org.zoundry.gaedt.services.IZGaeService#start()
     */
    public void start()
    {
        discoverApplications();
        registerWorkspaceListener();

        ILaunchManager launchManager = DebugPlugin.getDefault().getLaunchManager();
        launchManager.addLaunchListener(this);
    }

    /**
     * Stop life-cycle method - called when the plug-in activator stops.
     *
     * @see org.zoundry.gaedt.services.IZGaeService#stop()
     */
    public void stop()
    {
        ILaunchManager launchManager = DebugPlugin.getDefault().getLaunchManager();
        launchManager.removeLaunchListener(this);

        unregisterWorkspaceListener();
    }

    /**
     * @see org.zoundry.gaedt.services.applications.IZGaeApplicationsService#addObserver(org.zoundry.gaedt.services.applications.IZGaeApplicationsServiceObserver)
     */
    public void addObserver(IZGaeApplicationsServiceObserver aObserver)
    {
        getObservers().add(aObserver);
    }

    /**
     * @see org.zoundry.gaedt.services.applications.IZGaeApplicationsService#removeObserver(org.zoundry.gaedt.services.applications.IZGaeApplicationsServiceObserver)
     */
    public void removeObserver(IZGaeApplicationsServiceObserver aObserver)
    {
        getObservers().remove(aObserver);
    }

    /**
     * @see org.zoundry.gaedt.services.applications.IZGaeApplicationsService#startApplication(org.zoundry.gaedt.services.applications.IZGaeApplication, boolean)
     */
    public void startApplication(final IZGaeApplication aApplication, final boolean aDebugMode)
    {
        Job job = new Job("Starting GAE application '" + aApplication.getModel().getName() + "'")
        {
            /**
             * @see org.eclipse.core.runtime.jobs.Job#run(org.eclipse.core.runtime.IProgressMonitor)
             */
            @Override
            protected IStatus run(IProgressMonitor aMonitor)
            {
                if (aApplication.getStatus() == TZGaeApplicationStatus.STOPPED)
                {
                    IZGaeApplicationStarter starter = getOrCreateStarter(aApplication);
                    if (aDebugMode)
                        starter.startDebug();
                    else
                        starter.start();
                }
                return Status.OK_STATUS;
            }
        };
        job.setPriority(Job.INTERACTIVE);
        ISchedulingRule modifyRule = ZGaeSDKSchedulingRule.RULE;
        job.setRule(modifyRule);
        job.schedule();
    }

    /**
     * @see org.zoundry.gaedt.services.applications.IZGaeApplicationsService#updateApplication(org.zoundry.gaedt.services.applications.IZGaeApplication)
     */
    public void updateApplication(final IZGaeApplication aApplication)
    {
        Job job = new Job("Updating GAE application '" + aApplication.getModel().getName() + "'")
        {
            /**
             * @see org.eclipse.core.runtime.jobs.Job#run(org.eclipse.core.runtime.IProgressMonitor)
             */
            @Override
            protected IStatus run(IProgressMonitor aMonitor)
            {
                IZGaeApplicationUpdater updater = createUpdater(aApplication);
                updater.update();
                return Status.OK_STATUS;
            }
        };
        job.setPriority(Job.INTERACTIVE);
        ISchedulingRule modifyRule = ZGaeSDKSchedulingRule.RULE;
        job.setRule(modifyRule);
        job.schedule();
    }

    /**
     * @see org.zoundry.gaedt.services.applications.IZGaeApplicationsService#stopApplication(org.zoundry.gaedt.services.applications.IZGaeApplication)
     */
    public void stopApplication(IZGaeApplication aApplication)
    {
        if (aApplication.getStatus() == TZGaeApplicationStatus.RUNNING || aApplication.getStatus() == TZGaeApplicationStatus.DEBUGGING)
        {
            IZGaeApplicationStarter starter = getStarter(aApplication);
            if (starter != null)
                starter.stop();
        }
    }

    /**
     * Create an updater used to deploy the application to the Google App Engine.
     *
     * @param aApplication
     */
    protected IZGaeApplicationUpdater createUpdater(IZGaeApplication aApplication)
    {
        return ZGaeApplicationUpdaterFactory.getInstance().createUpdater(aApplication);
    }

    /**
     * Either gets an existing starter, or creates a new one.
     *
     * @param aApplication
     */
    protected IZGaeApplicationStarter getOrCreateStarter(IZGaeApplication aApplication)
    {
        IZGaeApplicationStarter starter = getStarter(aApplication);
        if (starter == null)
        {
            starter = ZGaeApplicationStarterFactory.getInstance().createStarter(aApplication);
            getStarters().add(starter);
        }
        return starter;
    }

    /**
     * Gets a cached starter for the given application.
     *
     * @param aApplication
     */
    protected IZGaeApplicationStarter getStarter(IZGaeApplication aApplication)
    {
        List<IZGaeApplicationStarter> starters = getStarters();
        for (IZGaeApplicationStarter starter : starters)
        {
            if (starter.matches(aApplication))
            {
                return starter;
            }
        }
        return null;
    }

    /**
     * Gets a cached starter for the given launch or null if none is found.
     *
     * @param aLaunch
     */
    protected IZGaeApplicationStarter getStarter(ILaunch aLaunch)
    {
        List<IZGaeApplicationStarter> starters = getStarters();
        for (IZGaeApplicationStarter starter : starters)
        {
            if (starter.matches(aLaunch))
            {
                return starter;
            }
        }
        return null;
    }

    /**
     * Discovers any Google App Engine applications in the Workspace.
     */
    protected void discoverApplications()
    {
        Job job = new Job("Discovering Google App Engine Applications")
        {
            /**
             * @see org.eclipse.core.runtime.jobs.Job#run(org.eclipse.core.runtime.IProgressMonitor)
             */
            @Override
            protected IStatus run(IProgressMonitor monitor)
            {
                doDiscoverApplications();
                return Status.OK_STATUS;
            }
        };
        job.setPriority(Job.LONG);
        IWorkspace workspace = ResourcesPlugin.getWorkspace();
        IResourceRuleFactory ruleFactory = workspace.getRuleFactory();
        ISchedulingRule modifyRule = ruleFactory.modifyRule(workspace.getRoot());
        job.setRule(modifyRule);
        job.schedule();
    }

    /**
     * Discovers any Google App Engine applications in the Workspace.
     */
    protected synchronized void doDiscoverApplications()
    {
        ZGaeApplicationFindingResourceVisitor visitor = new ZGaeApplicationFindingResourceVisitor();
        IWorkspace workspace = ResourcesPlugin.getWorkspace();
        try
        {
            workspace.getRoot().accept(visitor);
            getApplications().addAll(visitor.getApplications());
            updateObservers();
        }
        catch (CoreException e)
        {
            ZGaeUtil.logError(e);
        }
    }

    /**
     * Registers for changes to the workspace - this allows the application
     * service to dynamically change the list of available applications based on
     * changes to the workspace (new applications showing up, old ones getting
     * removed, etc).
     */
    protected void registerWorkspaceListener()
    {
        setResourceTracker(new ZGaeApplicationResourceTracker(this));
        ResourcesPlugin.getWorkspace().addResourceChangeListener(getResourceTracker());
    }

    /**
     * Unregister as a workspace listener.
     */
    private void unregisterWorkspaceListener()
    {
        ResourcesPlugin.getWorkspace().removeResourceChangeListener(getResourceTracker());
        setResourceTracker(null);
    }

    /**
     * Called by the resource tracker when an application resource is removed.
     *
     * @param aResource
     */
    public synchronized void onFileRemoved(IFile aResource)
    {
        // First, remove the application if the file is an app.yaml file.
        for (Iterator<IZGaeApplication> iter = getApplications().iterator(); iter.hasNext(); )
        {
            IZGaeApplication application = iter.next();
            if (application.getModel().getAppYaml().equals(aResource))
            {
                removeStarter(application);
                iter.remove();
            }
        }
        // Now, update the project applications - this is important if the file
        // that was removed was the gae.yaml file.
        refreshProjectApplications(aResource.getProject());
        updateObservers();
    }

    /**
     * Called by the resource tracker when an application resource changes.
     *
     * @param aResource
     */
    public synchronized void onFileChanged(IFile aResource)
    {
        refreshProjectApplications(aResource.getProject());
    }

    /**
     * Called by the resource tracker when an application resource is added.
     *
     * @param aResource
     */
    public synchronized void onFileAdded(IFile aResource)
    {
        refreshProjectApplications(aResource.getProject());
    }

    /**
     * Refresh all the applications found in the given project.
     *
     * @param aProject
     */
    protected void refreshProjectApplications(IProject aProject)
    {
        if (!aProject.exists())
            return;
        try
        {
            ZGaeApplicationFindingResourceVisitor visitor = new ZGaeApplicationFindingResourceVisitor();
            aProject.accept(visitor);
            Set<IZGaeApplication> foundApps = visitor.getApplications();
            // Remove all applications and re-add them.
            getApplications().removeAll(foundApps);
            getApplications().addAll(foundApps);
            // And remove any cached starters.
            for (IZGaeApplication application : foundApps)
                removeStarter(application);
            updateObservers();
        }
        catch (CoreException e)
        {
            ZGaeUtil.logError(e);
        }
    }

    /**
     * Update the observers.
     */
    protected void updateObservers()
    {
        List<IZGaeApplicationsServiceObserver> observers = getObservers();
        for (IZGaeApplicationsServiceObserver observer : observers)
        {
            observer.onUpdate(this);
        }
    }

    /**
     * Removes the starter associated with the given application.
     *
     * @param aApplication
     */
    private void removeStarter(IZGaeApplication aApplication)
    {
        IZGaeApplicationStarter starter = getStarter(aApplication);
        if (starter != null)
            getStarters().remove(starter);
    }

    /**
     * @see org.zoundry.gaedt.services.applications.IZGaeApplicationsService#getGoogleApplications()
     */
    public synchronized Set<IZGaeApplication> getGoogleApplications()
    {
        return new HashSet<IZGaeApplication>(getApplications());
    }

    /**
     * @return the applications
     */
    protected Set<IZGaeApplication> getApplications()
    {
        return mApplications;
    }

    /**
     * @param aApplications
     *            the applications to set
     */
    protected void setApplications(Set<IZGaeApplication> aApplications)
    {
        mApplications = aApplications;
    }

    /**
     * @return the resourceTracker
     */
    protected ZGaeApplicationResourceTracker getResourceTracker()
    {
        return mResourceTracker;
    }

    /**
     * @param aResourceTracker the resourceTracker to set
     */
    protected void setResourceTracker(ZGaeApplicationResourceTracker aResourceTracker)
    {
        mResourceTracker = aResourceTracker;
    }

    /**
     * @return the observers
     */
    protected List<IZGaeApplicationsServiceObserver> getObservers()
    {
        return mObservers;
    }

    /**
     * @param aObservers the observers to set
     */
    protected void setObservers(List<IZGaeApplicationsServiceObserver> aObservers)
    {
        mObservers = aObservers;
    }

    /**
     * @see org.eclipse.debug.core.ILaunchesListener2#launchesTerminated(org.eclipse.debug.core.ILaunch[])
     */
    public synchronized void launchesTerminated(ILaunch[] aLaunches)
    {
        boolean stateChanged = false;
        for (ILaunch launch : aLaunches)
        {
            IZGaeApplicationStarter starter = getStarter(launch);
            if (starter != null)
            {
                IZGaeApplicationInternal application = (IZGaeApplicationInternal) starter.getApplication();
                application.setStatus(TZGaeApplicationStatus.STOPPED);
                stateChanged = true;
            }
        }
        if (stateChanged)
            updateObservers();
    }

    /**
     * @see org.eclipse.debug.core.ILaunchesListener#launchesAdded(org.eclipse.debug.core.ILaunch[])
     */
    public synchronized void launchesAdded(ILaunch[] aLaunches)
    {
        boolean stateChanged = false;
        for (ILaunch launch : aLaunches)
        {
            IZGaeApplicationStarter starter = getStarter(launch);
            if (starter != null)
            {
                IZGaeApplicationInternal application = (IZGaeApplicationInternal) starter.getApplication();
                application.setStatus(TZGaeApplicationStatus.RUNNING);
                stateChanged = true;
            }
        }
        if (stateChanged)
            updateObservers();
    }

    /**
     * @see org.eclipse.debug.core.ILaunchesListener#launchesChanged(org.eclipse.debug.core.ILaunch[])
     */
    public void launchesChanged(ILaunch[] aLaunches)
    {
        // Don't care.
    }

    /**
     * @see org.eclipse.debug.core.ILaunchesListener#launchesRemoved(org.eclipse.debug.core.ILaunch[])
     */
    public void launchesRemoved(ILaunch[] aLaunches)
    {
        // Do we care about this?
    }

    /**
     * @return the starters
     */
    protected List<IZGaeApplicationStarter> getStarters()
    {
        return mStarters;
    }

    /**
     * @param aStarters the starters to set
     */
    protected void setStarters(List<IZGaeApplicationStarter> aStarters)
    {
        mStarters = aStarters;
    }
}
